package reports.display;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import common.StringOps;

/**
 * A generic table of data, with a title and column titles. The data is held as simple strings.
 * 
 * @author Ryan Cheatham
 * 
 *         {@invariant <tt>getColumnCount() > 0</tt>}
 * 
 *         {@invariant <tt>getRowCount() >= 0</tt>}
 */
public class ReportTable
{
  private String               title;
  private int                  columns;
  private String[]             columnTitles;

  private List<ReportTableRow> rows = new ArrayList<ReportTableRow>();

  /**
   * Constructs a new table with the specified title and number of columns, with empty column
   * titles.
   * 
   * @param title
   *          the title of the new table
   * @param columns
   *          the number of columns of the new table
   * 
   *          {@pre <tt>columns > 0</tt>}
   * 
   *          {@post <tt>getTitle() == title</tt>}
   * 
   *          {@post <tt>getColumnCount() == columns</tt>}
   * 
   * @throws IllegalArgumentException
   *           if the number of columns is less than 1.
   * 
   * @see ReportTable#setTitle(String)
   * @see ReportTable#setColumnCount(int)
   */
  public ReportTable(String title, int columns)
  {
    setTitle(title);

    if (columns < 1)
      throw new IllegalArgumentException("Invalid column count - count is less than 1!");

    this.columns = columns;
    columnTitles = new String[columns];
  }

  /**
   * Constructs a new table with the specified title and column titles.
   * 
   * @param title
   *          the title of the new table
   * @param columns
   *          the array of column titles
   * 
   *          {@pre <tt>columns != null</tt>}
   * 
   *          {@pre <tt>columns.length > 0</tt>}
   * 
   *          {@post <tt>getTitle() == title</tt>}
   * 
   *          {@post <tt>getColumnCount() == columns.length</tt>}
   * 
   *          {@post for each <tt>i</tt>, <tt>0 <= i < columns.length</tt>: getColumnTitle(i) ==
   *          columns[i]</tt>}
   * 
   * @throws IllegalArgumentException
   *           if the number of columns is less than 1.
   * @throws NullPointerException
   *           if columns is null.
   * 
   * @see ReportTable#setTitle(String)
   */
  public ReportTable(String title, String[] columns)
  {
    setTitle(title);

    if (columns.length < 1)
      throw new IllegalArgumentException("Invalid column count - count is less than 1!");

    this.columns = columns.length;
    columnTitles = Arrays.copyOf(columns, columns.length);
  }

  /**
   * Gets the title of this table.
   */
  public String getTitle()
  {
    return title;
  }

  /**
   * Returns whether this table has a title.
   */
  public boolean hasTitle()
  {
    return !StringOps.isNullOrEmpty(title);
  }

  /**
   * Sets the title of this table to the specified string.
   * 
   * @param title
   *          the new title
   * 
   *          {@post <tt>getTitle() == title</tt>}
   * 
   * @throws IllegalArgumentException
   *           if the title is null or an empty string.
   */
  public void setTitle(String title)
  {
    this.title = title;
  }

  /**
   * Gets the number of columns in this table.
   */
  public int getColumnCount()
  {
    return columns;
  }

  /**
   * Gets the title of the specified column.
   * 
   * @param column
   *          the column whose title is requested.
   * @return the title of the specified column.
   * 
   *         {@pre <tt>0 <= column < getColumnCount()</tt>}
   * 
   * @throws IndexOutOfBoundsException
   *           if <tt>column < 0</tt> or <tt>column >= getColumnCount()</tt>
   * 
   * @see ReportTable#getColumnCount()
   */
  public String getColumnTitle(int column)
  {
    return columnTitles[column];
  }

  /**
   * Gets an iterator through all the column titles.
   * 
   * {@post <tt>retval != null</tt>}
   * 
   * {@post <tt>retval.hasNext() == true</tt> before <tt>retval.next()</tt> is called}
   */
  public Iterator<String> getColumnTitles()
  {
    return Collections.unmodifiableList(Arrays.asList(columnTitles)).iterator();
  }

  /**
   * Sets the title of the specified column.
   * 
   * @param column
   *          the column whose title will be set.
   * @param title
   *          the new title of the column
   * 
   *          {@pre <tt>0 <= column < getColumnCount()</tt>}
   * 
   *          {@pre <tt>title != null</tt>}
   * 
   *          {@pre <tt>title.length() > 0</tt>}
   * 
   *          {@post <tt>getColumnTitle(column) == title</tt>}
   * 
   * @throws IllegalArgumentException
   *           if the title is null or empty.
   * 
   * @throws IndexOutOfBoundsException
   *           if <tt>column < 0</tt> or <tt>column >= getColumnCount()</tt>
   * 
   * @see ReportTable#getColumnCount()
   */
  public void setColumnTitle(int column, String title)
  {
    if (StringOps.isNullOrEmpty(title))
      throw new IllegalArgumentException("Invalid title - title is null or empty!");

    columnTitles[column] = title;
  }

  /**
   * Creates a new row of the table with empty data. It does NOT add it to its list; see
   * {@link #addRow(ReportTableRow)}.
   * 
   * @return a new row of this table.
   * 
   *         {@post <tt>retval != null</tt>}
   * 
   * @see ReportTable#addRow(ReportTableRow)
   */
  public ReportTableRow createRow()
  {
    return new ReportTableRow(this, columns);
  }

  /**
   * Gets the number of rows in this table.
   */
  public int getRowCount()
  {
    return rows.size();
  }

  /**
   * Adds the given table row to the end of this table
   * 
   * @param row
   * 
   *          {@pre row was created by this table's <tt>createRow()</tt> method}
   * 
   * @see ReportTable#createRow()
   */
  public void addRow(ReportTableRow row)
  {
    if (!equals(row.getTable()))
      throw new IllegalArgumentException("Invalid row - it was not created by this table!");

    rows.add(row);
  }

  /**
   * Returns whether this table contains the given row.
   * 
   * @param row
   *          the row that may be in this table.
   * @return whether this table contains the given row.
   */
  public boolean containsRow(ReportTableRow row)
  {
    if (row == null || this != row.getTable())
      return false;

    return rows.contains(row);
  }

  /**
   * Returns the row at the specified index.
   * 
   * @param row
   *          the index of the row to retrieve.
   * @return the row at the specified index.
   * 
   *         {@pre <tt>0 <= row < getRowCount()</tt>}
   * 
   *         {@post <tt>retval != null</tt>}
   * 
   * @throws IndexOutOfBoundsException
   *           if row is outside of range <tt>(0 <= row < getRowCount())</tt>
   */
  public ReportTableRow getRow(int row)
  {
    return rows.get(row);
  }

  /**
   * Returns an iterator through all the rows in the table.
   * 
   * {@post <tt>retval != null</tt>}
   */
  public Iterator<ReportTableRow> getRows()
  {
    return Collections.unmodifiableList(rows).iterator();
  }

  /**
   * Removes the specified row from this table. If the row is not contained in this table, no
   * changes are made.
   * 
   * @param row
   *          the row to remove.
   * 
   *          {@pre None}
   * 
   *          {@post The row is removed, if the row was contained in the table}
   */
  public void removeRow(ReportTableRow row)
  {
    rows.remove(row);
  }

  /**
   * Removes the specified row from this table.
   * 
   * @param row
   *          the index of the row to remove.
   * 
   *          {@pre <tt>0 <= row < getRowCount()</tt>}
   * 
   * @throws IndexOutOfBoundsException
   *           if <tt>row < 0</tt> or <tt>row >= getRowCount()</tt>
   */
  public void removeRow(int row)
  {
    rows.remove(row);
  }

  /**
   * Removes all rows in this table.
   */
  public void clearRows()
  {
    rows.clear();
  }

  @Override
  public String toString()
  {
    StringBuilder builder = new StringBuilder();
    builder.append("ReportTable [title=");
    builder.append(title);
    builder.append(", columns=");
    builder.append(columns);
    builder.append(", columnTitles=");
    builder.append(Arrays.toString(columnTitles));
    builder.append("]");
    return builder.toString();
  }

  public String prettyPrint()
  {
    StringBuilder builder = new StringBuilder();

    int[] widths = new int[columns];

    for (int i = 0; i < columns; i++)
    {
      String title = columnTitles[i].replace('\n', ' ');
      widths[i] = title.length();
      builder.append("| ");
      builder.append(title);
      builder.append(' ');
    }
    builder.append("|\n");

    int width = 1;
    for (int i = 0; i < widths.length; i++)
      width += widths[i] + 3;

    StringBuilder b = new StringBuilder(width);
    for (int i = 0; i < width; i++)
      b.append('-');
    builder.append(b);
    builder.append('\n');

    for (ReportTableRow row : rows)
    {
      buildRow(row, width, builder, widths);
    }

    b = new StringBuilder(width);
    if (title == null)
    {
      for (int i = 0; i < width; i++)
        b.append('=');
    }
    else
    {
      int widthLeft = (width - title.length() - 2) / 2;
      int widthRight = widthLeft + ((width + title.length()) % 2);
      for (int i = 0; i < widthLeft; i++)
        b.append('=');
      b.append(' ');
      b.append(title);
      b.append(' ');
      for (int i = 0; i < widthRight; i++)
        b.append('=');
    }

    builder.insert(0, '\n');
    builder.insert(0, b);

    return builder.toString();
  }

  private void buildRow(ReportTableRow row, int width, StringBuilder builder, int[] widths)
  {
    StringBuilder[] lines = new StringBuilder[1];
    lines[0] = new StringBuilder();

    for (int i = 0; i < columns; i++)
      lines = buildColumn(lines, row.getCell(i), i, width, builder, widths);

    for (int i = 0; i < lines.length; i++)
    {
      lines[i].append("|\n");
      builder.append(lines[i]);
    }

    StringBuilder b = new StringBuilder(width);
    for (int i = 0; i < width; i++)
      b.append('-');
    builder.append(b);
    builder.append('\n');
  }

  private StringBuilder[] buildColumn(StringBuilder[] lines, String data, int i, int width,
      StringBuilder builder, int[] widths)
  {

    String[] colLines;
    if (data.contains("\n"))
      colLines = data.split("\n");
    else
      colLines = data.split("(?<=\\G.{" + widths[i] + "})");

    if (colLines.length > lines.length)
    {
      int l = lines.length;
      lines = Arrays.copyOf(lines, colLines.length);

      String filler = "";
      for (int j = 0; j < i; j++)
      {
        filler += "| ";
        filler += String.format("%" + widths[j] + "s", " ");
        filler += " ";
      }

      for (int j = l; j < lines.length; j++)
        lines[j] = new StringBuilder(filler);
    }
    else if (colLines.length < lines.length)
    {
      int l = colLines.length;
      colLines = Arrays.copyOf(colLines, lines.length);

      for (int j = l; j < colLines.length; j++)
        colLines[j] = "";
    }

    for (int l = 0; l < lines.length; l++)
      lines[l].append(String.format("| %" + widths[i] + "s ", colLines[l]));

    return lines;
  }

  @Override
  public int hashCode()
  {
    final int prime = 31;
    int result = 1;
    result = prime * result + Arrays.hashCode(columnTitles);
    result = prime * result + ((title == null) ? 0 : title.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj)
  {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (!(obj instanceof ReportTable))
      return false;
    ReportTable other = (ReportTable) obj;
    if (!Arrays.equals(columnTitles, other.columnTitles))
      return false;
    if (title == null)
    {
      if (other.title != null)
        return false;
    }
    else if (!title.equals(other.title))
      return false;
    return true;
  }
}